home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Resources / Developers / XAMPP 1.5.4 / Windows installer / xampp-win32-1.5.4-installer.exe / xampp / php / pear / File / CSV.php next >
Encoding:
PHP Script  |  2005-09-26  |  16.4 KB  |  514 lines

  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  3.  
  4. /**
  5.  * File::CSV
  6.  *
  7.  * PHP versions 4 and 5
  8.  *
  9.  * LICENSE: This source file is subject to version 3.0 of the PHP license
  10.  * that is available through the world-wide-web at the following URI:
  11.  * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
  12.  * the PHP License and are unable to obtain it through the web, please
  13.  * send a note to license@php.net so we can mail you a copy immediately.
  14.  *
  15.  * @category    File
  16.  * @package     File
  17.  * @author      Tomas V.V.Cox <cox@idecnet.com>
  18.  * @author      Helgi ▐ormar <dufuz@php.net>
  19.  * @copyright   2004-2005 The Authors
  20.  * @license     http://www.php.net/license/3_0.txt  PHP License 3.0
  21.  * @version     CVS: $Id: CSV.php,v 1.24 2005/08/09 08:16:02 dufuz Exp $
  22.  * @link        http://pear.php.net/package/File
  23.  */
  24.  
  25. require_once 'PEAR.php';
  26. require_once 'File.php';
  27.  
  28. /**
  29. * File class for handling CSV files (Comma Separated Values), a common format
  30. * for exchanging data.
  31. *
  32. * TODO:
  33. *  - Usage example and Doc
  34. *  - Use getPointer() in discoverFormat
  35. *  - Add a line counter for being able to output better error reports
  36. *  - Store the last error in GLOBALS and add File_CSV::getLastError()
  37. *
  38. * Wish:
  39. *  - Other methods like readAll(), writeAll(), numFields(), numRows()
  40. *  - Try to detect if a CSV has header or not in discoverFormat()
  41. *
  42. * Known Bugs:
  43. * (they has been analyzed but for the moment the impact in the speed for
  44. *  properly handle this uncommon cases is too high and won't be supported)
  45. *  - A field which is composed only by a single quoted separator (ie -> ;";";)
  46. *    is not handled properly
  47. *  - When there is exactly one field minus than the expected number and there
  48. *    is a field with a separator inside, the parser will throw the "wrong count" error
  49. *
  50. * @author Tomas V.V.Cox <cox@idecnet.com>
  51. * @author      Helgi ▐ormar <dufuz@php.net>
  52. * @package File
  53. */
  54. class File_CSV
  55. {
  56.     /**
  57.     * This raiseError method works in a different way. It will always return
  58.     * false (an error occurred) but it will call PEAR::raiseError() before
  59.     * it. If no default PEAR global handler is set, will trigger an error.
  60.     *
  61.     * @param string $error The error message
  62.     * @return bool always false
  63.     */
  64.     function raiseError($error)
  65.     {
  66.         // If a default PEAR Error handler is not set trigger the error
  67.         // XXX Add a PEAR::isSetHandler() method?
  68.         if ($GLOBALS['_PEAR_default_error_mode'] == PEAR_ERROR_RETURN) {
  69.             PEAR::raiseError($error, null, PEAR_ERROR_TRIGGER, E_USER_WARNING);
  70.         } else {
  71.             PEAR::raiseError($error);
  72.         }
  73.         return false;
  74.     }
  75.  
  76.     /**
  77.     * Checks the configuration given by the user
  78.     *
  79.     * @access private
  80.     * @param string &$error The error will be written here if any
  81.     * @param array  &$conf  The configuration assoc array
  82.     * @return string error    Returns a error message
  83.     */
  84.     function _conf(&$error, &$conf)
  85.     {
  86.         // check conf
  87.         if (!is_array($conf)) {
  88.             return $error = 'Invalid configuration';
  89.         }
  90.  
  91.         if (!isset($conf['fields']) || !is_numeric($conf['fields'])) {
  92.             return $error = 'The number of fields must be numeric (the "fields" key)';
  93.         }
  94.  
  95.         if (isset($conf['sep'])) {
  96.             if (strlen($conf['sep']) != 1) {
  97.                 return $error = 'Separator can only be one char';
  98.             }
  99.         } elseif ($conf['fields'] > 1) {
  100.             return $error = 'Missing separator (the "sep" key)';
  101.         }
  102.  
  103.         if (isset($conf['quote'])) {
  104.             if (strlen($conf['quote']) != 1) {
  105.                 return $error = 'The quote char must be one char (the "quote" key)';
  106.             }
  107.         } else {
  108.             $conf['quote'] = null;
  109.         }
  110.  
  111.         if (!isset($conf['crlf'])) {
  112.             $conf['crlf'] = "\n";
  113.         }
  114.  
  115.         if (!isset($conf['eol2unix'])) {
  116.             $conf['eol2unix'] = true;
  117.         }
  118.     }
  119.  
  120.     /**
  121.     * Return or create the file descriptor associated with a file
  122.     *
  123.     * @param string $file The name of the file
  124.     * @param array  &$conf The configuration
  125.     * @param string $mode The open node (ex: FILE_MODE_READ or FILE_MODE_WRITE)
  126.     * @param boolean $reset if passed as true and resource for the file exists
  127.     *                       than the file pointer will be moved to the beginning
  128.     *
  129.     * @return mixed A file resource or false
  130.     */
  131.     function getPointer($file, &$conf, $mode = FILE_MODE_READ, $reset = false)
  132.     {
  133.         static $resources  = array();
  134.         static $config;
  135.         if (isset($resources[$file])) {
  136.             $conf = $config;
  137.             if ($reset) {
  138.                 fseek($resources[$file], 0);
  139.             }
  140.             return $resources[$file];
  141.         }
  142.         File_CSV::_conf($error, $conf);
  143.         if ($error) {
  144.             return File_CSV::raiseError($error);
  145.         }
  146.         $config = $conf;
  147.         PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
  148.         $fp = &File::_getFilePointer($file, $mode);
  149.         PEAR::popErrorHandling();
  150.         if (PEAR::isError($fp)) {
  151.             return File_CSV::raiseError($fp);
  152.         }
  153.         $resources[$file] = $fp;
  154.  
  155.         if ($mode == FILE_MODE_READ && !empty($conf['header'])) {
  156.             if (!File_CSV::read($file, $conf)) {
  157.                 return false;
  158.             }
  159.         }
  160.         return $fp;
  161.     }
  162.  
  163.     /**
  164.     * Unquote data
  165.     *
  166.     * @param string $field The data to unquote
  167.     * @param string $quote The quote char
  168.     * @return string the unquoted data
  169.     */
  170.     function unquote($field, $quote)
  171.     {
  172.         // Trim first the string.
  173.         $field = trim($field);
  174.         $quote = trim($quote);
  175.  
  176.         // Incase null fields (form: ;;)
  177.         if (!strlen($field)) {
  178.             return $field;
  179.         }
  180.  
  181.         if ($quote && $field{0} == $quote && $field{strlen($field)-1} == $quote) {
  182.             return substr($field, 1, -1);
  183.         }
  184.         return $field;
  185.     }
  186.  
  187.     /**
  188.     * Reads a row of data as an array from a CSV file. It's able to
  189.     * read memo fields with multiline data.
  190.     *
  191.     * @param string $file   The filename where to write the data
  192.     * @param array  &$conf   The configuration of the dest CSV
  193.     *
  194.     * @return mixed Array with the data read or false on error/no more data
  195.     */
  196.     function readQuoted($file, &$conf)
  197.     {
  198.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_READ)) {
  199.             return false;
  200.         }
  201.  
  202.         $buff = $c = '';
  203.         $ret  = array();
  204.         $i = 1;
  205.         $in_quote = false;
  206.         $quote = $conf['quote'];
  207.         $f = $conf['fields'];
  208.         $eol2unix = $conf['eol2unix'];
  209.         while (($ch = fgetc($fp)) !== false) {
  210.             $prev = $c;
  211.             $c = $ch;
  212.             // Common case
  213.             if ($c != $quote && $c != $conf['sep'] && $c != "\n" && $c != "\r") {
  214.                 $buff .= $c;
  215.                 continue;
  216.             }
  217.  
  218.             // Start quote.
  219.             if ($quote && $c == $quote &&
  220.                 ($prev == $conf['sep'] || $prev == "\n" || $prev === null ||
  221.                  $prev == "\r" || $prev == ''))
  222.             {
  223.                 $in_quote = true;
  224.             }
  225.  
  226.             if ($in_quote) {
  227.                 // When ends quote
  228.                 if ($c == $conf['sep'] && $prev == $conf['quote']) {
  229.                     $in_quote = false;
  230.                 } elseif ($c == "\n" || $c == "\r") {
  231.                     $sub = ($prev == "\r") ? 2 : 1;
  232.                     if ((strlen($buff) >= $sub) &&
  233.                         ($buff{strlen($buff) - $sub} == $quote))
  234.                     {
  235.                         $in_quote = false;
  236.                     }
  237.                 }
  238.             }
  239.  
  240.             if (!$in_quote && ($c == $conf['sep'] || $c == "\n" || $c == "\r") && $prev != '') {
  241.                 // More fields than expected
  242.                 if (($c == $conf['sep']) && ((count($ret) + 1) == $f)) {
  243.                     // Seek the pointer into linebreak character.
  244.                     while (true) {
  245.                         $c = fgetc($fp);
  246.                         if  ($c == "\n" || $c == "\r") {
  247.                             break;
  248.                         }
  249.                     }
  250.  
  251.                     // Insert last field value.
  252.                     $ret[] = File_CSV::unquote($buff, $quote);
  253.                     return $ret;
  254.                 }
  255.  
  256.                 // Less fields than expected
  257.                 if (($c == "\n" || $c == "\r") && ($i != $f)) {
  258.                     // Insert last field value.
  259.                     $ret[] = File_CSV::unquote($buff, $quote);
  260.  
  261.                     // Pair the array elements to fields count.
  262.                     return array_merge($ret,
  263.                                        array_fill(count($ret),
  264.                                                  ($f - 1) - (count($ret) - 1),
  265.                                                  '')
  266.                     );
  267.                 }
  268.  
  269.                 if ($prev == "\r") {
  270.                     $buff = substr($buff, 0, -1);
  271.                 }
  272.  
  273.                 // Convert EOL character to Unix EOL (LF).
  274.                 if ($eol2unix) {
  275.                     $buff = preg_replace('/(\r\n|\r)$/', "\n", $buff);
  276.                 }
  277.  
  278.                 $ret[] = File_CSV::unquote($buff, $quote);
  279.                 if (count($ret) == $f) {
  280.                     return $ret;
  281.                 }
  282.                 $buff = '';
  283.                 $i++;
  284.                 continue;
  285.             }
  286.             $buff .= $c;
  287.         }
  288.         return !feof($fp) ? $ret : false;
  289.     }
  290.  
  291.     /**
  292.     * Reads a "row" from a CSV file and return it as an array
  293.     *
  294.     * @param string $file The CSV file
  295.     * @param array  &$conf The configuration of the dest CSV
  296.     *
  297.     * @return mixed Array or false
  298.     */
  299.     function read($file, &$conf)
  300.     {
  301.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_READ)) {
  302.             return false;
  303.         }
  304.         // The size is limited to 4K
  305.         if (!$line   = fgets($fp, 4096)) {
  306.             return false;
  307.         }
  308.  
  309.         $fields = $conf['fields'] == 1 ? array($line) : explode($conf['sep'], $line);
  310.  
  311.         if ($conf['quote']) {
  312.             $last =& $fields[count($fields) - 1];
  313.             // Fallback to read the line with readQuoted when guess
  314.             // that the simple explode won't work right
  315.             if (($last{strlen($last) - 1} == "\n"
  316.                 && $last{0} == $conf['quote']
  317.                 && $last{strlen(rtrim($last)) - 1} != $conf['quote'])
  318.                 ||
  319.                 (count($fields) != $conf['fields'])
  320.                 // XXX perhaps there is a separator inside a quoted field
  321.                 //preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line)
  322.                 )
  323.             {
  324.                 fseek($fp, -1 * strlen($line), SEEK_CUR);
  325.                 return File_CSV::readQuoted($file, $conf);
  326.             } else {
  327.                 $last = rtrim($last);
  328.                 foreach ($fields as $k => $v) {
  329.                     $fields[$k] = File_CSV::unquote($v, $conf['quote']);
  330.                 }
  331.             }
  332.         }
  333.  
  334.         if (count($fields) != $conf['fields']) {
  335.             File_CSV::raiseError("Read wrong fields number count: '". count($fields) .
  336.                                   "' expected ".$conf['fields']);
  337.             return true;
  338.         }
  339.         return $fields;
  340.     }
  341.  
  342.     /**
  343.     * Internal use only, will be removed in the future
  344.     *
  345.     * @param string $str The string to debug
  346.     * @access private
  347.     */
  348.     function _dbgBuff($str)
  349.     {
  350.         if (strpos($str, "\r") !== false) {
  351.             $str = str_replace("\r", "_r_", $str);
  352.         }
  353.         if (strpos($str, "\n") !== false) {
  354.             $str = str_replace("\n", "_n_", $str);
  355.         }
  356.         if (strpos($str, "\t") !== false) {
  357.             $str = str_replace("\t", "_t_", $str);
  358.         }
  359.         echo "buff: ($str)\n";
  360.     }
  361.  
  362.     /**
  363.     * Writes a struc (array) in a file as CSV
  364.     *
  365.     * @param string $file   The filename where to write the data
  366.     * @param array  $fields Ordered array with the data
  367.     * @param array  &$conf   The configuration of the dest CSV
  368.     *
  369.     * @return bool True on success false otherwise
  370.     */
  371.     function write($file, $fields, &$conf)
  372.     {
  373.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_WRITE)) {
  374.             return false;
  375.         }
  376.         if (count($fields) != $conf['fields']) {
  377.             File_CSV::raiseError("Wrong fields number count: '". count($fields) .
  378.                                   "' expected ".$conf['fields']);
  379.             return true;
  380.         }
  381.         $write = '';
  382.         for ($i = 0; $i < count($fields); $i++) {
  383.             if (!is_numeric($fields[$i]) && $conf['quote']) {
  384.                 $write .= $conf['quote'] . $fields[$i] . $conf['quote'];
  385.             } else {
  386.                 $write .= $fields[$i];
  387.             }
  388.             if ($i < (count($fields) - 1)) {
  389.                 $write .= $conf['sep'];
  390.             } else {
  391.                 $write .= $conf['crlf'];
  392.             }
  393.         }
  394.         if (!fwrite($fp, $write)) {
  395.             return File_CSV::raiseError('Can not write to file');
  396.         }
  397.         return true;
  398.     }
  399.  
  400.     /**
  401.     * Discover the format of a CSV file (the number of fields, the separator
  402.     * and if it quote string fields)
  403.     *
  404.     * @param string the CSV file name
  405.     * @param array extra separators that should be checked for.
  406.     * @return mixed Assoc array or false
  407.     */
  408.     function discoverFormat($file, $extraSeps = array())
  409.     {
  410.         if (!$fp = @fopen($file, 'r')) {
  411.             return File_CSV::raiseError("Could not open file: $file");
  412.         }
  413.         $seps = array("\t", ';', ':', ',');
  414.         $seps = array_merge($seps, $extraSeps);
  415.         $matches = array();
  416.  
  417.         // Set auto detect line ending for Mac EOL support if < PHP 4.3.0.
  418.         $phpver = version_compare('4.3.0', phpversion(), '<');
  419.         if ($phpver) {
  420.             $oldini = ini_get('auto_detect_line_endings');
  421.             ini_set('auto_detect_line_endings', '1');
  422.         }
  423.  
  424.         // Take the first 10 lines and store the number of ocurrences
  425.         // for each separator in each line
  426.  
  427.         $lines = file($file);
  428.         if (count($lines) > 10) {
  429.             $lines = array_slice($lines, 0, 10);
  430.         }
  431.  
  432.         if ($phpver) {
  433.             ini_set('auto_detect_line_endings', $oldini);
  434.         }
  435.  
  436.         foreach ($lines as $line) {
  437.             foreach ($seps as $sep) {
  438.                 $matches[$sep][] = substr_count($line, $sep);
  439.             }
  440.         }
  441.  
  442.         $final = array();
  443.         // Group the results by amount of equal ocurrences
  444.         foreach ($matches as $sep => $res) {
  445.             $times = array();
  446.             $times[0] = 0;
  447.             foreach ($res as $k => $num) {
  448.                 if ($num > 0) {
  449.                     $times[$num] = (isset($times[$num])) ? $times[$num] + 1 : 1;
  450.                 }
  451.             }
  452.             arsort($times);
  453.  
  454.             // Use max fields count.
  455.             $fields[$sep] = max(array_flip($times));
  456.             $amount[$sep] = $times[key($times)];
  457.         }
  458.  
  459.         arsort($amount);
  460.         $sep    = key($amount);
  461.  
  462.         $conf['fields'] = $fields[$sep] + 1;
  463.         $conf['sep']    = $sep;
  464.  
  465.         // Test if there are fields with quotes arround in the first 5 lines
  466.         $quotes = '"\'';
  467.         $quote  = null;
  468.         if (count($lines) > 5) {
  469.             $lines = array_slice($lines, 0, 5);
  470.         }
  471.  
  472.         foreach ($lines as $line) {
  473.             if (preg_match("|$sep([$quotes]).*([$quotes])$sep|U", $line, $match)) {
  474.                 if ($match[1] == $match[2]) {
  475.                     $quote = $match[1];
  476.                     break;
  477.                 }
  478.             }
  479.             if (preg_match("|^([$quotes]).*([$quotes])$sep{0,1}|", $line, $match)
  480.                 || preg_match("|([$quotes]).*([$quotes])$sep\s$|Us", $line, $match))
  481.             {
  482.                 if ($match[1] == $match[2]) {
  483.                     $quote = $match[1];
  484.                     break;
  485.                 }
  486.             }
  487.         }
  488.         $conf['quote'] = $quote;
  489.         fclose($fp);
  490.         // XXX What about trying to discover the "header"?
  491.         return $conf;
  492.     }
  493.  
  494.     /**
  495.      * Front to call getPointer and moving the resource to the
  496.      * beginning of the file
  497.      * Reset it if you like.
  498.      *
  499.      * @param string $file The name of the file
  500.      * @param array  &$conf The configuration
  501.      * @param string $mode The open node (ex: FILE_MODE_READ or FILE_MODE_WRITE)
  502.      *
  503.      * @return boolean true on success false on failure
  504.      */
  505.     function resetPointer($file, &$conf, $mode)
  506.     {
  507.         if (!File_CSV::getPointer($file, $conf, $mode, true)) {
  508.             return false;
  509.         }
  510.  
  511.         return true;
  512.     }
  513. }
  514. ?>